package org.haptimap.hcimodules.touchovermap;

import java.util.List;

import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.location.Location;
import android.os.Vibrator;
import android.speech.tts.TextToSpeech;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * TouchOverlayView provides haptic and/or acoustic feedback when given Lines are touched.
 */
public class TouchOverlayView extends View {
	
	final String LOG_TAG = "TOUCHOVERLAY-VIEW";
	
	// Settings
	private boolean vibrationEnabled = true;
	private boolean speechInterruptEnabled = false;
	private boolean active = true;
	private boolean spoken = false;
	
	final int MAX_POINT_CNT = 4;
	
	float[] x = new float[MAX_POINT_CNT];
	float[] y = new float[MAX_POINT_CNT];
	boolean[] isTouch = new boolean[MAX_POINT_CNT];
	
	// Tolerance for touch events
	private int pixdistance = 30;
	
	// Speech
	private TextToSpeech tts;
	
	// Projection used to map coordinates to pixels and vice versa
	private Projection projection;
	
	private LinesProvider linesProvider;
	
	public TouchOverlayView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	public TouchOverlayView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public TouchOverlayView(Context context) {
		super(context);
	}
	
	public void initialize(Projection projection, LinesProvider provider, int pixdistance) {
		this.setProjection(projection);
		this.setLinesProvider(provider);
		this.pixdistance = pixdistance;
	}
	
	/**
	 * Checks whether a line was touched and acts accordingly by vibration and/or speech.
	 * 
	 * @see android.view.View#onTouchEvent(android.view.MotionEvent)
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if (!active || linesProvider == null)
			return false;
		
		Point display = null;
		Line tl = null;
		int id = 0;
		
		Log.d( LOG_TAG, "Entrou");		
		
		int pointerIndex = ((event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT);
		int pointerId = event.getPointerId(pointerIndex);
		int action = (event.getAction() & MotionEvent.ACTION_MASK);
		int pointCnt = event.getPointerCount();
		Log.d( LOG_TAG, "PointerCount : " + pointCnt);
			    
		if (pointCnt <= MAX_POINT_CNT){
			if (pointerIndex <= MAX_POINT_CNT - 1){							 
			    for (int i = 0; i < pointCnt; i++) {
			    	id = event.getPointerId(i);
			    	x[id] = (int)event.getX(i);
			    	y[id] = (int)event.getY(i);
			    	display = new Point((int)x[id], (int)y[id]);
			    	
			    	switch (action){
				    	case MotionEvent.ACTION_DOWN:{				    		
				    		tl = this.getTouchedLine(display);
				    		Log.d( LOG_TAG, "DOWN - Index/ID : " + pointerIndex + "/" + id + " - X,Y:" + display.x + "," + display.y);
						    break;
				    	}			      
					    
				    	case MotionEvent.ACTION_POINTER_DOWN:{				    		
				    		tl = this.getTouchedLine(display);
				    		Log.d( LOG_TAG, "POINTER_DOWN - ID : " + pointerIndex + "/" + id + "- X,Y:" + display.x + "," + display.y);
				    		break;
				    	}
					    case MotionEvent.ACTION_MOVE:{					    	
					    	tl = this.getTouchedLine(display);
					    	Log.d( LOG_TAG, "MOVE - ID : " + pointerIndex + "/" + id + "- X,Y:" + x[id] + "," + y[id]);
						    break;
					    }
					      
					    case MotionEvent.ACTION_UP:{					    	
					    	Log.d( LOG_TAG, "UP - ID : " + pointerIndex + "/" + id + "- X,Y:" + x[id] + "," + y[id]);
					    	break;
					    }
					     case MotionEvent.ACTION_POINTER_UP:{					    	
					    	Log.d( LOG_TAG, "POINTER_UP - ID : " + pointerIndex + "/" + id + "- X,Y:" + x[id] + "," + y[id]);
					    	break;
					     }
					     case MotionEvent.ACTION_CANCEL:{					    	
					    	Log.d( LOG_TAG, "CANCEL - ID : " + pointerIndex + "/" + id + "- X,Y:" + x[id] + "," + y[id]);
					    	break;
					     }
					     default:					    	
			    	}//switch
			    	if (tl!=null){
			    		
			    		break;
			    	}
			    	
			    }//for
			    if(tl!=null) {
					
					if (spoken==false){
						speakLine(tl);
						Log.d( LOG_TAG, "FOUND ");
					}			
					vibrate();
					tl = null;
					
					this.spoken = true;
				} else {
					// stop/interrupt everything
					stopVibrate();
					stopSpeak();
					this.spoken = false; 
				}
		    }//if
		}//if
			
		return true;
	}

	private void stopVibrate() {
		Vibrator v = (Vibrator) this.getContext().getSystemService(Context.VIBRATOR_SERVICE);
		if(this.vibrationEnabled) {
			v.cancel();
		}
	}

	private void vibrate() {
		Vibrator v = (Vibrator) this.getContext().getSystemService(Context.VIBRATOR_SERVICE);
		if(this.vibrationEnabled) {
			// vibrate
			// Start immediately
			// Vibrate for 5000 milliseconds
			// Never sleep
			long[] pattern = { 0, 5000, 0 };
			v.vibrate(pattern,0);
		}
	}

	private void stopSpeak() {
		if(this.tts!=null) {
			this.tts.stop();
		}
	}

	private void speakLine(Line tl) {
		// speak, if tts was provided
		if(this.tts!=null && ((!this.tts.isSpeaking() || this.speechInterruptEnabled) &&
				(tl.getName()!=null))) {
			String toSpeak = "Unknown";
			if(tl.getName()!=null) {
				toSpeak = tl.getName();
			}
			
			this.tts.speak(toSpeak, TextToSpeech.QUEUE_FLUSH, null);
		}
	}

	/**
	 * Indicates whether vibration on touching a line is enabled.
	 * 
	 * @return true when vibration is enabled, false otherwise.
	 */
	public boolean isVibrationEnabled() {
		return vibrationEnabled;
	}

	/**
	 * Sets whether the device should vibrate when a line is touched.
	 * 
	 * @param vibrationEnabled true to enable vibration, false otherwise
	 */
	public void setVibrationEnabled(boolean vibrationEnabled) {
		this.vibrationEnabled = vibrationEnabled;
	}

	
	/**
	 * Indicates whether speech will be interrupted.
	 * 
	 * @return true when speech interruption is enabled, false otherwise
	 */
	public boolean isSpeechInterruptEnabled() {
		return speechInterruptEnabled;
	}

	/**
	 * Sets whether speech will be interrupted when necessary.
	 * 
	 * @param speechInterruptEnabled true to enable speech interruption, false otherwise
	 */
	public void setSpeechInterruptEnabled(boolean speechInterruptEnabled) {
		this.speechInterruptEnabled = speechInterruptEnabled;
	}
	
	public TextToSpeech getTts() {
		return tts;
	}
	
	public void setTts(TextToSpeech tts) {
		this.tts = tts;
	}

	
	/**
	 * Returns the used projection to map coordinated to pixels and vice versa.
	 * 
	 * @return
	 */
	public Projection getProjection() {
		return projection;
	}
	
	
	/**
	 * Sets the projection which is used to map coordinates to pixels and vice versa.
	 * 
	 * @param projection
	 */
	public void setProjection(Projection projection) {
		this.projection = projection;
	}
	
	public LinesProvider getLinesProvider() {
		return linesProvider;
	}

	public void setLinesProvider(LinesProvider linesProvider) {
		this.linesProvider = linesProvider;
	}

	/**
	 * Indicates whether the View is active and therefore handling touch events.
	 * 
	 * Notice: No TouchEvent will be propagated to underlying views when active.
	 * 
	 * @return true when active, false otherwise
	 */
	public boolean isActive() {
		return active;
	}
	
	/**
	 * Sets whether the view is active and therefore handling touch events.
	 * 
	 * Notice: No TouchEvent will be propagated to underlying views when active.
	 * This includes e.g. an underlying MapView and its controls.
	 * 
	 * @param active true to activate the view, false otherwise
	 */
	public void setActive(boolean active) {
		this.active = active;
	}

	/*Another answer:
	* P is the point.
	* C is a corner of the rectangle.
	* v1 and v2 are the two vectors that define the sides (with C as origin).
	* v = P-C

	P is in the rectangle if and only if
	0<=dot_product(v,v1)<=dot_product(v1,v1) and 0<=dot_product(v,v2)<=dot_product(v2,v2)*/
	private Line getTouchedLine(Point touch) {
		if (linesProvider == null || projection == null)
			return null;
		
		// Determine size of view in order to determine what needs to be displayed
		Location tl = projection.fromPixels(this.getLeft(), this.getTop());
		Location br = projection.fromPixels(this.getRight(), this.getBottom());
		
		List<Line> lines = linesProvider.receiveLines(tl.getLatitude(), tl.getLongitude(), br.getLatitude(), br.getLongitude());
		
		if (lines == null)
			return null;
		
		synchronized (lines) {
			for (Line line : lines) {				
				
				//Calculate distance for lines (way data)
				if (line.getElements().size()>1){
										 
					Log.d( LOG_TAG, "Line Touched ");
					List<Point> ps = line.getElementsAsDisplayCoordinatesIfCloseToPoint(projection, touch,pixdistance);
				 	for (int i = 1; i < ps.size(); i++) {
				 		Point p = ps.get(i - 1);
						Point n = ps.get(i);

						if (!p.equals(n)) {
							// Deltas bestimmen
							int dX = n.x - p.x;
							int dY = n.y - p.y;

							// richtungs, und normalisierter richtungs- und
							// normalenvektor
							Point rvA = new Point(dX, dY);
							Point rvB = new Point(-rvA.y, rvA.x); // normalenvektor
																		// zu rvA

							double rvBL = Math.sqrt(rvB.x * rvB.x + rvB.y * rvB.y);
							PointF rvBN = new PointF((float) (rvB.x / rvBL),(float) (rvB.y / rvBL));
								
							// TODO: Review tolerance
							int tolerance = 48;

							rvB.x = (int) (rvBN.x * tolerance); // cutting to a
																	// fixed length
							rvB.y = (int) (rvBN.y * tolerance);

							// Eckpunkt rotiertes Rechteck
							Point c = new Point((int) (p.x - rvB.x / 2), // rvBN
										(int) (p.y - rvB.y / 2));// rvBN

							// Vektor Eckpunkt zu Punkt
							Point v = new Point(touch.x - c.x, touch.y - c.y);

							// Skalarprodukte
							float sA = dot_product(v, rvB);
							float sB = dot_product(rvB, rvB);

							if (0 <= sA && sA <= sB) {
								float sC = dot_product(v, rvA);
								float sD = dot_product(rvA, rvA);

								if (0 <= sC && sC <= sD) {
									return line;
								}
							} // fi
						} // fi: same points
					} // points
				 }
				 //Calculate distance for points (node data)
				else if (line.getElements().size()==1){
					// alternative, slower method...
					List<Point> ps = line.getElementsAsDisplayCoordinates(projection);
					Log.d( LOG_TAG, "Point Touched ");
				 
					for (int i = 0; i < ps.size(); i++) {										 				
						Point p = ps.get(i);
						if (Line.distanceInScreenPix(p, touch)<=this.pixdistance){
							return line;
						}					
					 }
				}

			} // lines

			return null;
		} // sync
	}

	private float dot_product(Point a, Point b) {
		return a.x*b.x+a.y*b.y;
	}

}
